명령어 수준 병렬성
1. 개요
1. 개요
명령어 수준 병렬성은 컴퓨터 프로그램 내의 일련의 명령어를 병렬 또는 동시에 실행하는 기술이다. 이는 단일 프로세서 코어 내에서 명령어 실행 속도를 높여 CPU의 성능을 향상시키는 것이 주요 목적이다. 이 개념은 병렬 컴퓨팅의 한 형태로, 프로그램 카운터가 가리키는 하나의 명령어 흐름 내에서 병렬성을 찾아내는 것을 의미한다.
이를 구현하는 주요 접근 방식은 하드웨어 기반의 동적 병렬 처리와 소프트웨어 (컴파일러) 기반의 정적 병렬 처리로 나뉜다. 주요 구현 기법으로는 명령어 파이프라이닝, 슈퍼스칼라, 비순차적 명령어 처리 등이 있다. 이러한 기법들은 파이프라인 위험이라는 성능 저하 요소를 관리하면서 적용된다.
명령어 수준 병렬성은 병행성 또는 태스크 수준 병렬성과 구별된다. 후자가 여러 스레드나 프로세스를 동시에 실행하는 것에 초점을 맞춘다면, 명령어 수준 병렬성은 단일 스레드의 실행 효율을 극대화하는 데 주력한다. 현대 마이크로프로세서의 성능 발전은 클럭 속도 향상뿐만 아니라 이러한 명령어 수준 병렬성 기술의 정교화를 통해 이루어져 왔다.
2. 기본 개념
2. 기본 개념
2.1. 정의와 목적
2.1. 정의와 목적
명령어 수준 병렬성은 컴퓨터 프로그램 내에서 순차적으로 존재하는 명령어들을 병렬 또는 동시에 실행하는 기술이다. 이는 단일 프로세서 코어 내에서 명령어 실행 속도를 높여 CPU의 성능을 향상시키는 것이 주요 목적이다. 기본적으로 프로그램 카운터가 가리키는 순서대로 명령어를 하나씩 처리하는 방식보다, 여러 명령어를 겹쳐서 처리함으로써 클럭 주기당 처리량을 높인다.
이를 구현하는 주요 접근 방식은 크게 두 가지로 나뉜다. 하나는 컴파일러가 프로그램 실행 전에 병렬 실행 가능한 명령어를 분석하고 스케줄링하는 정적 다중 내보내기 방식이며, 다른 하나는 하드웨어가 실행 시간에 명령어의 의존성을 동적으로 분석하여 병렬 실행을 결정하는 동적 다중 내보내기 방식이다. 후자의 대표적인 예가 슈퍼스칼라 아키텍처이다.
명령어 수준 병렬성은 병행성과 구별되는 개념이다. 명령어 수준 병렬성은 단일 스레드 내에서 명령어 실행의 효율성을 높이는 데 초점을 맞춘다. 반면, 병행성은 운영체제가 여러 스레드나 프로세스를 CPU 코어에 할당하여 동시에 실행하는 것을 포함한다. 즉, 명령어 수준 병렬성은 하나의 작업 흐름을 더 빠르게 만드는 기술이라면, 병행성은 여러 작업을 동시에 진행하는 기술이다.
2.2. 병행성과의 차이
2.2. 병행성과의 차이
명령어 수준 병렬성과 병행성은 모두 컴퓨팅 성능을 높이기 위한 병렬 처리 개념이지만, 그 범위와 목적에서 명확한 차이가 있다.
명령어 수준 병렬성은 단일 프로세스 내에서, 더 정확히는 단일 스레드의 명령어 실행 흐름 내에서 병렬성을 찾아내는 기법이다. 이는 CPU가 하나의 프로그램 카운터가 가리키는 명령어 흐름 속에서, 서로 의존성이 없는 여러 명령어를 동시에 실행하거나 파이프라인을 통해 겹쳐 실행함으로써 성능을 향상시킨다. 주요 구현 기법으로는 명령어 파이프라이닝, 슈퍼스칼라, 비순차적 명령어 처리 등이 있으며, 이는 모두 하드웨어나 컴파일러가 단일 작업의 실행 속도를 높이기 위해 내부적으로 처리한다.
반면, 병행성은 운영체제 수준에서 여러 개의 독립적인 실행 단위를 관리하는 개념이다. 여기에는 여러 프로세스나 여러 스레드를 단일 또는 다수의 CPU 코어에 할당하여 교대로 실행하거나(시분할), 실제로 동시에 실행하는 것이 포함된다. 병행성의 목적은 단일 작업의 속도를 높이는 것보다는, 멀티태스킹 환경에서 여러 응용 프로그램이 동시에 실행되는 것처럼 보이게 하거나, 하나의 애플리케이션을 여러 작업 단위로 나누어 처리하는 데 있다.
요약하면, 명령어 수준 병렬성은 '어떻게 하면 하나의 작업을 더 빠르게 끝낼 수 있을까'에 초점을 맞춘 마이크로아키텍처적 접근법이고, 병행성은 '어떻게 하면 여러 작업을 효율적으로 함께 처리할 수 있을까'에 초점을 맞춘 시스템 수준의 매크로적 접근법이다. 전자는 주로 프로세서 설계와 컴파일러 최적화의 영역이며, 후자는 운영체제와 응용 프로그램 설계의 영역에 해당한다.
3. 구현 접근 방식
3. 구현 접근 방식
3.1. 정적 다중 내보내기 (컴파일러 기반)
3.1. 정적 다중 내보내기 (컴파일러 기반)
정적 다중 내보내기는 명령어 수준 병렬성을 구현하는 주요 접근 방식 중 하나로, 컴파일러가 프로그램 실행 전에 병렬로 실행할 명령어들을 결정하고 스케줄링하는 소프트웨어 기반 방법이다. 이 방식은 동적 다중 내보내기와 대비되는 개념으로, 런타임에 하드웨어가 병렬성을 탐지하는 대신 컴파일 타임에 모든 결정이 이루어진다. 컴파일러는 소스 코드를 분석하여 서로 데이터 의존성이 없는 명령어들을 찾아내고, 이를 하나의 실행 묶음인 내보내기 패킷으로 구성한다.
이 접근법의 대표적인 예로 VLIW 아키텍처와 EPIC 아키텍처가 있다. 이러한 아키텍처에서는 컴파일러가 생성한 내보내기 패킷이 프로세서에 의해 그대로 실행되며, 하드웨어는 복잡한 명령어 스케줄링 로직을 갖추지 않아도 된다. 이로 인해 하드웨어 설계가 단순해지고 전력 소모를 줄일 수 있는 장점이 있다. 그러나 컴파일러가 모든 실행 상황을 정확히 예측하기 어렵고, 서로 다른 마이크로아키텍처마다 최적의 스케줄링이 달라 호환성 문제가 발생할 수 있는 한계도 존재한다.
3.2. 동적 다중 내보내기 (하드웨어 기반)
3.2. 동적 다중 내보내기 (하드웨어 기반)
동적 다중 내보내기는 명령어 수준 병렬성을 구현하는 주요 하드웨어 접근 방식이다. 이 방식은 프로세서가 프로그램 실행 중, 즉 런타임에 병렬로 실행할 명령어를 결정하고 스케줄링한다. 이는 컴파일 시점에 병렬성을 결정하는 정적 다중 내보내기와 대비되는 개념이다. 동적 방식의 핵심 장점은 데이터 의존성이나 분기 예측 실패와 같은 런타임 상황에 유연하게 대응하여 파이프라인의 유휴 시간을 최소화할 수 있다는 점이다.
이 접근법을 구현하는 대표적인 프로세서 아키텍처가 슈퍼스칼라이다. 슈퍼스칼라 프로세서는 하드웨어 내에 여러 개의 파이프라인 또는 실행 유닛을 갖추고, 매 클럭 사이클마다 명령어를 분석하여 의존성이 없는 여러 명령어를 동시에 각 실행 유닛에 발행한다. 이를 통해 이론상 CPI를 1보다 작게 만들어 성능을 극대화한다. 또한, 비순차적 명령어 처리 기법을 함께 사용하여 명령어가 프로그램 순서와 다르게 실행되도록 하여 데이터 위험이나 구조적 위험으로 인한 지연을 회피한다.
동적 다중 내보내기를 위한 하드웨어는 복잡한 구조를 가진다. 명령어 윈도우나 재배치 버퍼와 같은 구조를 통해 비순차적으로 실행 완료된 명령어들을 최종적으로는 원래 프로그램 순서대로 결과를 커밋한다. 또한, 레지스터 리네이밍 기술을 통해 명령어 간의 의존성을 하드웨어 수준에서 제거한다. 이러한 복잡한 하드웨어 로직은 전력 소모와 칩 면적 증가를 초래하는 단점이 있다.
이 방식의 성능은 프로그램 내에 존재하는 병렬성의 양에 크게 의존한다. 메모리 접근 지연이나 예측하기 어려운 분기 명령어, 강한 데이터 의존성을 가진 코드는 하드웨어가 많은 명령어를 동시에 발행하는 것을 방해한다. 따라서 컴파일러의 최적화와 함께 하드웨어의 동적 스케줄링 능력이 조화를 이루어야 높은 성능을 얻을 수 있다.
4. 주요 기법
4. 주요 기법
4.1. 명령어 파이프라이닝
4.1. 명령어 파이프라이닝
명령어 파이프라이닝은 명령어 수준 병렬성을 구현하는 가장 기본적인 기법이다. 이 기법은 하나의 명령어 처리 과정을 여러 단계로 나누고, 서로 다른 명령어들이 각 단계를 겹쳐서 실행되도록 설계한다. 마치 컨베이어 벨트처럼, 한 명령어가 실행 단계에 있을 때 다음 명령어는 해석 단계를, 그다음 명령어는 인출 단계를 동시에 진행하는 방식으로 CPU의 활용률을 극대화한다.
전통적으로 명령어 처리 과정은 인출, 해석, 실행, 저장과 같은 단계로 구분된다. 명령어 파이프라이닝을 사용하지 않으면 프로세서는 한 명령어의 모든 단계가 완료될 때까지 다음 명령어 처리를 시작할 수 없어 자원이 낭비된다. 파이프라이닝을 적용하면 이론적으로 각 단계를 처리하는 하드웨어 모듈이 지속적으로 작업을 수행하게 되어 전체적인 처리 속도가 크게 향상된다.
그러나 파이프라이닝의 성능은 이상적인 조건에서만 발휘되며, 실제로는 여러 가지 위험 요인으로 인해 파이프라인이 멈추거나 지연되는 경우가 발생한다. 이러한 위험은 크게 데이터 위험, 제어 위험, 구조 위험으로 분류된다. 데이터 위험은 명령어 간의 데이터 의존성 때문에 발생하며, 제어 위험은 분기 명령어로 인한 프로그램 흐름의 갑작스러운 변화에서 비롯된다. 구조 위험은 하드웨어 자원의 한계로 인해 여러 명령어가 동일한 기능 유닛을 동시에 사용하려 할 때 나타난다.
이러한 위험을 극복하고 파이프라이닝의 효율을 더욱 높이기 위해 후속 기술들이 개발되었다. 슈퍼스칼라 방식은 하나의 파이프라인이 아닌 여러 개의 파이프라인을 두어 한 클럭 사이클에 다수의 명령어를 동시에 처리한다. 또한 비순차적 명령어 처리는 명령어 간 의존성이 없는 경우 하드웨어가 명령어 실행 순서를 재배치하여 파이프라인의 정체를 최소화하는 고급 기법이다.
4.2. 슈퍼스칼라
4.2. 슈퍼스칼라
슈퍼스칼라(superscalar)는 명령어 수준 병렬성을 구현하는 핵심 기법 중 하나이다. 이는 하나의 프로세서 코어 내에서 단일 클럭 사이클에 둘 이상의 명령어를 동시에 인출하고 실행할 수 있는 마이크로아키텍처 설계 방식을 의미한다. 기본적인 명령어 파이프라인이 하나의 파이프라인만을 통해 명령어를 순차적으로 처리하는 반면, 슈퍼스칼라 프로세서는 여러 개의 파이프라인을 병렬로 운영하여 처리량을 극대화한다.
슈퍼스칼라 방식의 동작은 주로 하드웨어에 의해 동적으로 제어된다. 프로세서는 실행 시점에 명령어 스트림을 분석하여 서로 데이터 의존성이 없는 명령어들을 선별하고, 이를 사용 가능한 여러 개의 실행 유닛에 분배하여 동시에 처리한다. 이때 정수 연산 유닛, 부동소수점 연산 유닛, 로드 스토어 유닛 등과 같은 다양한 기능 유닛이 병렬로 작동한다. 이러한 동적 스케줄링은 비순차적 명령어 처리 기법과 결합되어 사용되는 경우가 많다.
슈퍼스칼라 설계의 성능은 프로그램 내에 존재하는 병렬성의 정도에 크게 의존한다. 이상적인 경우 CPI가 1보다 작아질 수 있지만, 명령어 간의 제어 의존성이나 자원 경합으로 인해 이론적인 성능에 도달하기는 어렵다. 대부분의 현대 고성능 마이크로프로세서, 예를 들어 인텔 코어 시리즈나 AMD 라이젠 시리즈와 같은 범용 CPU들은 슈퍼스칼라 방식을 채택하고 있다.
4.3. 비순차적 명령어 처리
4.3. 비순차적 명령어 처리
비순차적 명령어 처리는 명령어 수준 병렬성을 높이기 위한 고급 기법으로, 프로그램의 원래 순서와 상관없이 명령어 간의 데이터 의존성이 없는 경우 그 실행 순서를 바꾸어 파이프라인의 유휴 시간을 최소화한다. 이 기법은 슈퍼스칼라 프로세서에서 동적으로 명령어를 스케줄링하는 핵심 메커니즘이다. 컴파일러가 정적으로 명령어 순서를 재배치하는 것과 달리, 하드웨어가 런타임에 명령어의 종속성을 분석하고 실행 가능한 명령어를 즉시 처리한다.
이를 구현하기 위한 핵심 하드웨어 구조로는 대기 영역과 재정렬 버퍼가 있다. 명령어는 인출 및 해독 단계를 거친 후 대기 영역에 배치된다. 명령어 실행에 필요한 모든 피연산자가 준비되면(이는 레지스터 파일이나 다른 실행 유닛의 결과로부터 전방 전달을 통해 얻을 수 있음), 해당 기능 유닛에서 실행된다. 실행이 완료된 결과는 재정렬 버퍼에 임시 저장되었다가, 원래 프로그램 순서에 맞게 레지스터나 메모리에 최종 기록된다. 이 과정을 순차 결과 쓰기라고 한다.
비순차적 명령어 처리는 데이터 위험으로 인한 파이프라인 정지를 효과적으로 완화한다. 예를 들어, 긴 지연 시간을 가지는 메모리 적재 명령어 뒤에 오는, 해당 결과와 무관한 명령어들을 미리 실행할 수 있게 함으로써 전체 처리량을 높인다. 이 기법은 분기 예측 및 동적 스케줄링과 결합되어 현대 고성능 마이크로프로세서의 성능을 견인하는 주요 요소이다.
5. 파이프라인 위험
5. 파이프라인 위험
5.1. 데이터 위험
5.1. 데이터 위험
데이터 위험은 명령어 파이프라인에서 발생하는 주요 위험 중 하나로, 서로 다른 명령어 간에 데이터 의존성이 존재할 때 발생한다. 이는 한 명령어의 실행 결과가 아직 레지스터나 메모리에 반영되지 않았는데, 그 결과값을 필요로 하는 후속 명령어가 실행되어야 하는 상황에서 생긴다. 예를 들어, 첫 번째 명령어가 어떤 레지스터에 값을 쓰기 전에, 두 번째 명령어가 그 레지스터에서 값을 읽으려고 하면 잘못된 데이터를 사용하게 되어 오류가 발생할 수 있다.
이러한 데이터 위험을 해결하기 위한 주요 기법으로는 전방 전달과 명령어 스케줄링이 있다. 전방 전달은 파이프라인의 중간 단계에서 생성된 결과를, 쓰기 단계를 기다리지 않고 즉시 후속 명령어의 실행 단계로 전달하는 하드웨어 기법이다. 명령어 스케줄링은 컴파일러나 프로세서가 명령어들의 실행 순서를 의존성이 없는 방향으로 재배치하여 데이터 위험으로 인한 대기 시간을 최소화한다.
데이터 위험은 그 의존성의 성격에 따라 다시 읽기 후 쓰기, 쓰기 후 읽기, 쓰기 후 쓰기 위험으로 세분화될 수 있다. 현대 슈퍼스칼라 프로세서나 비순차적 명령어 처리 아키텍처는 이러한 데이터 위험을 하드웨어 수준에서 동적으로 탐지하고 전방 전달 등의 기법을 통해 극복함으로써 명령어 수준 병렬성을 높인다.
5.2. 제어 위험
5.2. 제어 위험
제어 위험은 프로그램 카운터의 갑작스러운 변화로 인해 발생하는 파이프라인 위험이다. 이는 주로 분기 명령어, 점프 명령어, 서브루틴 호출 또는 인터럽트와 같이 프로그램의 실행 흐름이 예상과 달리 변경될 때 나타난다. 파이프라인은 일반적으로 다음에 실행할 명령어를 순차적으로 미리 인출하여 채워 넣는데, 제어 흐름이 변경되면 이미 파이프라인에 들어가 처리 중이던 명령어들이 무효화되기 때문이다. 이로 인해 파이프라인이 일시 중단되고, 새로운 명령어 주소부터 파이프라인을 다시 채워야 하므로 성능 손실이 발생한다.
제어 위험을 완화하기 위한 주요 기법으로는 분기 예측이 있다. 분기 예측은 하드웨어 또는 소프트웨어가 분기 명령어의 결과(점프할지 말지)를 미리 추측하여, 추측된 경로의 명령어를 파이프라인에 미리 인출하고 실행하는 기술이다. 예측이 성공하면 파이프라인이 중단되지 않고 계속 진행되어 성능 저하를 방지할 수 있다. 대표적인 분기 예측 방식으로는 정적 분기 예측과 동적 분기 예측이 있으며, 동적 분기 예측은 분기 목적지 버퍼와 같은 하드웨어를 사용해 과거의 분기 역사를 기반으로 더 정확하게 예측한다.
또 다른 기법으로는 분기 지연 슬롯이 있다. 이는 컴파일러가 분기 명령어 뒤에 오는 하나 이상의 명령어 위치를 재배치하여, 분기 명령어가 실제로 실행되는 동안 파이프라인이 유용한 작업을 계속하도록 하는 소프트웨어적 방법이다. 그러나 이 기법은 컴파일러의 복잡성을 증가시키고 모든 경우에 효과적인 명령어를 찾기 어려울 수 있다. 더 진보된 접근 방식으로는 조기 분기 해결이 있는데, 이는 비순차적 명령어 처리 아키텍처에서 분기 조건을 가능한 한 빨리 평가하여 잘못된 경로로의 실행을 최소화하는 것이다.
5.3. 구조 위험
5.3. 구조 위험
구조 위험은 명령어 파이프라인에서 서로 다른 명령어가 프로세서의 동일한 하드웨어 자원을 동시에 사용하려고 할 때 발생하는 충돌이다. 이는 파이프라인 위험의 주요 유형 중 하나로, 하드웨어 자원이 제한되어 있어 병렬 처리가 방해받는 상황을 의미한다. 예를 들어, 메모리 접근을 담당하는 버스가 하나뿐이라면, 한 명령어가 인출 단계에서 메모리를 읽는 동안 다른 명령어가 저장 단계에서 메모리에 쓸 수 없어 파이프라인이 정지하게 된다.
구조 위험의 전형적인 예는 단일 포트를 가진 레지스터 파일이나 단일 ALU를 사용하는 경우이다. 만약 슈퍼스칼라 방식으로 두 개의 명령어를 동시에 실행하려 할 때, 두 명령어 모두 실행 단계에서 하나의 ALU를 필요로 한다면, 한 명령어는 대기해야 하므로 병렬 처리가 지연된다. 또한, 명령어 캐시와 데이터 캐시가 분리되지 않은 통합 캐시 구조에서도 인출과 데이터 접근이 충돌할 수 있다.
이러한 위험을 해결하기 위한 주요 방법은 하드웨어 자원을 중복 구성하는 것이다. 예를 들어, 레지스터 파일의 읽기/쓰기 포트를 추가하거나, 독립적인 정수 연산 장치와 부동소수점 연산 장치를 별도로 두는 방식이다. 하버드 아키텍처처럼 명령어와 데이터를 위한 별도의 메모리 경로를 제공하는 것도 구조 위험을 줄이는 고전적인 설계 방식이다. 현대 마이크로프로세서에서는 이러한 자원 중복과 세심한 파이프라인 설계를 통해 구조 위험을 최소화하고 있다.
6. 성능과 한계
6. 성능과 한계
명령어 수준 병렬성의 성능 향상은 암달의 법칙에 의해 제한된다. 프로그램 내에서 순차적으로 실행되어야 하는 부분이 존재하면, 그 부분은 병렬화할 수 없으며 이는 전체 성능 향상의 상한선이 된다. 또한 파이프라인 위험으로 인해 이론적인 최대 성능에 도달하기 어렵다. 데이터 위험과 제어 위험은 하드웨어나 컴파일러에 의한 최적화 기법(예: 레지스터 리네이밍, 분기 예측)으로 완화할 수 있지만, 완전히 제거할 수는 없다.
명령어 수준 병렬성의 효율적인 활용을 위한 주요 기법으로는 명령어 파이프라이닝, 슈퍼스칼라, 비순차적 명령어 처리 등이 있다. 특히 슈퍼스칼라와 비순차적 명령어 처리를 구현하는 하드웨어는 매우 복잡해지며, 전력 소비와 발열 문제를 동반한다. 또한, 병렬로 실행할 수 있는 독립적인 명령어의 수는 프로그램의 고유 특성에 의해 제한된다. 순환문 펼치기와 같은 컴파일러 최적화 기법은 이를 개선하기 위해 사용된다.
명령어 수준 병렬성의 한계는 현대 마이크로프로세서 설계에서 뚜렷하게 나타난다. 1990년대와 2000년대 초반에는 슈퍼스칼라 방식을 통해 클럭당 명령어 처리 횟수를 극대화하는 데 주력했으나, 복잡도와 전력 효율의 한계에 부딪혔다. 이로 인해 산업계의 초점은 단일 코어 내의 명령어 수준 병렬성 향상에서 멀티코어 프로세서를 통한 스레드 수준 병렬성으로 전환되었다. 오늘날의 고성능 CPU는 제한된 명령어 수준 병렬성 기법과 다수의 코어를 결합하여 전체적인 성능을 끌어올린다.
